﻿#region licence
//  Copyright 2008 Michael Cromwell

//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at

//       http://www.apache.org/licenses/LICENSE-2.0

//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using log4net.Core;
using Rhino.DSL;
using log4net.Util;
using System.Reflection;
using log4net.Repository.Hierarchy;
using System.IO;
using System.Threading;

namespace log4net.altconf
{
    /// <summary>
    /// Use this class to initialize the log4net environment using a DSL file
    /// <remarks>
    /// <para>
    /// Configures a <see cref="DslRepository"/> using a DSL file
    /// </para>
    /// </remarks>
    /// </summary>
    public static class DslConfigurator
    {
        private static DslFactory dslFactory;
        private static readonly Type declaringType = typeof(DslConfigurator);
        private const string rootLoggerName = "root";
        private static ConfigurationFileWatcher configWatcher;
        private static readonly object locker = new object();
        
        static DslConfigurator()
        {
            dslFactory = new DslFactory();
            dslFactory.Register<Log4NetDslConfiguration>(new Log4NetDslEngine());
            
        }
        
        /// <summary>
        /// Directory where DSL file resides
        /// </summary>
        public static string BaseDirectory
        {
            get { return dslFactory.BaseDirectory; }
            set { dslFactory.BaseDirectory = value; }
        }

        /// <summary>
        /// Configures the <see cref="DslRepository"/> using the passed DSL file.
        /// <remarks>
        /// <para>
        /// DSL file will be monitored for changes
        /// </para>
        /// <para>
        /// Threadsafe
        /// </para>
        /// <para>Should not throw any exception</para>
        /// </remarks>
        /// </summary>
        /// <param name="dslFile">file to use for configuration</param>
        public static void Configure(string dslFile)
        {
            if (string.IsNullOrEmpty(dslFile))
            {
                LogLog.Error("dslFile supplied was empty or null.");
                return;
            }

            LoggerManager.RepositorySelector = new DefaultRepositorySelector(typeof(DslRepository));
            InternalConfiguration(dslFile);
        }

        /// <summary>
        /// <para>Performs actual configuration using settings from passed DSL file</para>
        /// <remarks>
        /// <para>Threadsafe</para>
        /// </remarks>
        /// </summary>
        /// <param name="dslFile">file with config settings</param>
        private static void InternalConfiguration(string dslFile)
        {
            lock (locker)
            {
                try
                {
                    ReleaseConfigFile();
                    var config = dslFactory.Create<Log4NetDslConfiguration>(dslFile, null);
                    config.Process();

                    var loggerRepository = LogManager.GetRepository(Assembly.GetCallingAssembly()) as IDslRepositoryConfigurator;
                    if (loggerRepository == null)
                        LogLog.Warn("DslConfigurator: Repository [" + loggerRepository + "] does not support the DslConfigurator");

                    // tell the repository to configure itself
                    loggerRepository.Configure(config);
                    
                }
                catch (Exception thrownException)
                {
                    // don't bubble up instead log internally
                    LogLog.Error("Exception raised inside [DslConfigurator.InternalConfiguration(dslFile)]", thrownException);
                }
                finally
                {
                    // keep a watch on the config file
                    WatchConfigFile(dslFile);
                }
            }
        }

        private static void ReleaseConfigFile()
        {
            // release existing if one exists
            if (configWatcher != null)
            {
                configWatcher.Dispose();
                configWatcher = null;
            }
        }

        private static void WatchConfigFile(string dslFile)
        {
            configWatcher = new ConfigurationFileWatcher();
            configWatcher.Initialize(dslFile);
        }

        #region configuration file watcher

        /// <summary>
        /// Used to help keep track of a configuration file using
        /// <see cref="FileSystemWatcher"/>
        /// </summary>
        public class ConfigurationFileWatcher : IDisposable
        {
            protected FileSystemWatcher watcher;
            protected Timer changeCoordinator;
            protected string fileToWatch = string.Empty;
            private static Type declaringType = typeof(ConfigurationFileWatcher);
            private const int timeoutMilliseconds = 500;

            ~ConfigurationFileWatcher()
            {
                Dispose(false);
            }

            public void Initialize(string fileToWatch)
            {
                this.fileToWatch = fileToWatch;
                watcher = new FileSystemWatcher();
                watcher.Path = DslConfigurator.BaseDirectory;
                watcher.Filter = fileToWatch;
                watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName;

                watcher.Changed += new FileSystemEventHandler(ConfigFileChanged);
                watcher.Deleted += new FileSystemEventHandler(ConfigFileChanged);
                watcher.Created += new FileSystemEventHandler(ConfigFileChanged);
                watcher.Renamed += new RenamedEventHandler(ConfigFileRenamed);

                watcher.EnableRaisingEvents = true;
                changeCoordinator = new Timer(new TimerCallback(OnWatchedFileChange), null, Timeout.Infinite, Timeout.Infinite);

            }

            private void ConfigFileChanged(object sender, FileSystemEventArgs e)
            {
                LogLog.Debug("ConfigurationFileWatcher: " + e.ChangeType + " [" + fileToWatch + "]");
                changeCoordinator.Change(timeoutMilliseconds, Timeout.Infinite);
            }

            private void ConfigFileRenamed(object sender, RenamedEventArgs e)
            {
                LogLog.Debug("ConfigurationFileWatcher: " + e.ChangeType + " [" + fileToWatch + "]");
                fileToWatch = e.Name; // set the config file to the new renamed filename
                changeCoordinator.Change(timeoutMilliseconds, Timeout.Infinite);
            }

            private void OnWatchedFileChange(object state)
            {
                DslConfigurator.InternalConfiguration(fileToWatch);
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected void Dispose(bool managed)
            {
                if (managed)
                {
                    if (watcher != null)
                    {
                        watcher.EnableRaisingEvents = false;
                        watcher.Dispose();
                    }

                    if (changeCoordinator != null)
                        changeCoordinator.Dispose();
                }
            }

        }

        #endregion
    }


}
